Struct Vs Class When To Use Which Β· How it works

5 min read
Mid-level7 min read
Rapid overview

How it works

_Facts checked against Microsoft Learn: "Structure types", "Choosing Between Class and Struct" (Framework Design Guidelines), 2026-09-22._

Reference semantics vs value semantics

A variable of a class type holds a reference to an instance on the managed heap. Assigning it, passing it or returning it copies the reference, so two variables can point at the same object and a change through one is visible through the other.

A variable of a struct type holds the instance itself. Assignment, passing an argument and returning a value each copy the whole instance, so every variable owns an independent copy. Microsoft's reference page says it plainly: because structs have value semantics, define them immutable.

"Structs live on the stack" is a simplification

A struct lives inline in whatever contains it:

Where the struct is declaredWhere its bytes are
Local variable / parameterThe method's stack frame (or a CPU register after JIT)
Field of a classInside that class instance, on the heap
Element of Point[]Inline in the array's heap block β€” no per-element object
Captured by a lambda, or a local in an async/iterator methodHoisted into a compiler-generated class β†’ heap
Cast to object or to an interface it implementsBoxed: a new heap object holding a copy

Framework Design Guidelines: value types are "allocated either on the stack or inline in containing types and deallocated when the stack unwinds or when their containing type gets deallocated." So the useful claim is "no separate allocation", not "always on the stack".

Boxing

Converting a struct to object, ValueType or an interface allocates a box on the heap and copies the value into it; unboxing copies it back out. Mutating the box never touches the original. Common hidden sources: non-generic collections (ArrayList), string.Format(object) overloads, calling an interface method through the interface type, and the default ValueType.Equals(object). Generics with a where T : struct or IEquatable<T> constraint avoid it.

Nullability

A struct variable can't be null; its "empty" state is default(T) β€” every field zeroed. To model "missing", use Nullable<T> (T?), which adds a HasValue flag around the value. A class variable can be null, and with nullable reference types enabled the compiler tracks that for you.

Inheritance limits

A struct implicitly derives from System.ValueType, is implicitly sealed, can't inherit from another struct or class, and can't be a base type. It can implement interfaces. It can't declare a finalizer. So: no virtual/protected members, no polymorphic hierarchy.

Constructors, default, and the C# 10–12 changes

  • Every struct has a public parameterless constructor. From C# 10 you may write your own, but it must be public.
  • default(T) and array creation (new T[30]) ignore that constructor and produce the all-zero value. Only new T() runs it.
  • If a struct has a field initializer it must declare at least one constructor.
  • C# 11 (auto-default structs): a constructor no longer has to assign every field; unassigned fields are zeroed for you.
  • C# 12: structs can have primary constructors.

readonly struct, readonly members, record struct

  • readonly struct: every field must be readonly and every property get-only or init. The compiler then knows no member mutates this, so it can skip defensive copies when the struct is reached through an in parameter or a readonly field. It's shallow: a List<T> field can't be replaced, but you can still Add to it.
  • readonly on an individual method/property marks just that member as non-mutating.
  • record struct (C# 10): the compiler generates value equality (Equals, GetHashCode, ==, !=, IEquatable<T>), ToString, and Deconstruct. Its positional properties are read-write; use readonly record struct to make them init-only.
  • with expressions work on any struct (C# 10), not just records: var p2 = p1 with { X = 3 };.

When to choose which (Microsoft's guidance)

The Framework Design Guidelines say: CONSIDER a struct if instances are small and commonly short-lived or commonly embedded in other objects. AVOID a struct unless the type has all of these:

  1. It logically represents a single value, like a primitive (int, double).
  2. Its instance size is under 16 bytes.
  3. It is immutable.
  4. It will not have to be boxed frequently.

"In all other cases, you should define your types as classes." The page itself notes it is reprinted from the 2008 second edition and that the book has since been fully revised in a third edition; treat 16 bytes as a rule of thumb for copy cost, and measure (BenchmarkDotNet) before going over it. ValueTuple, Span<T>, DateTime, Guid (16 bytes) and decimal (16 bytes) are the BCL's own examples.

🧩 Real-World Example (HFM context)

If you’re processing millions of tick messages per second:

  • Use a struct (or readonly struct) for individual ticks (lightweight, immutable).
  • Use a class for services and entities that manage state, like OrderBook, TradeSession, or CacheManager.

Size check: on 64-bit this Tick is 24 bytes (an 8-byte string reference + two 8-byte doubles), above Microsoft's "under 16 bytes" guideline. It can still be the right call for a hot path β€” no per-tick allocation β€” but pass it with in to avoid copying 24 bytes per call, and measure. Replacing Symbol with a small integer id brings it back under 16.

Example:

readonly struct Tick
{
    public string Symbol { get; init; }
    public double Bid { get; init; }
    public double Ask { get; init; }
}

class PriceFeedProcessor
{
    private readonly List<Tick> _ticks = new();

    public void OnTick(Tick tick) => _ticks.Add(tick);
}

See also